//@version=6
// ============================================================================
//  Breakout & Retest Buy Sell Signals with TP & SL [LunqFX]
// ----------------------------------------------------------------------------
//  A breakout is not the entry. The entry is the RETEST: price breaks a level,
//  comes back to it, and the level holds from the other side. Most breakout
//  tools stop at the break. This one waits for the return, confirms the hold
//  on a closed candle and prints the trade: entry, stop beyond the retest,
//  three targets and the lot size for your account.
//
//  NON-REPAINTING: levels come from confirmed swing points, and every break,
//  retest, signal, target and stop is decided on a closed bar.
// ============================================================================
indicator("Breakout & Retest Buy Sell Signals with TP & SL [LunqFX]", "Breakout & Retest", overlay = true,
     max_lines_count = 200, max_labels_count = 200, max_boxes_count = 100)

// ─────────────────────────────────────────────────────────────────────────
//  PALETTE — turquoise / violet
// ─────────────────────────────────────────────────────────────────────────
TURQ  = #00E5D0
VIOL  = #B026FF
GOLD  = #FFD250
RISK  = #FF4D6D
CARD  = #131A24
CARD2 = #1B2431
HEAD  = #0E141D
TXT   = #F1F5F9
MUTE  = #94A3B8

// ─────────────────────────────────────────────────────────────────────────
//  INPUTS
// ─────────────────────────────────────────────────────────────────────────
gL      = "Levels"
pivLen  = input.int(10, "Level strength (swing length)", minval = 3, maxval = 50, group = gL, tooltip = "Bars on each side that make a swing high or low. Every confirmed swing becomes a level. Higher = fewer, more important levels.")
maxLv   = input.int(8, "Levels tracked", minval = 2, maxval = 20, group = gL)
tolK    = input.float(0.20, "Retest zone (× ATR)", minval = 0.05, maxval = 1.0, step = 0.05, group = gL, tooltip = "Half-height of the retest zone drawn around a broken level. Price must come back into it for the return to count as a retest.")
brkK    = input.float(0.10, "Break buffer (× ATR)", minval = 0.0, maxval = 1.0, step = 0.05, group = gL, tooltip = "A break needs a close beyond the level by at least this much — a close an inch through is not a break.")
winN    = input.int(20, "Bars allowed for the retest", minval = 3, maxval = 100, group = gL, tooltip = "If price does not come back within this many bars after the break, the zone expires.")
trendOn = input.bool(true, "Trade with the trend only", group = gL, tooltip = "Buys only while price is above the trend EMA, sells only while it is below. Retests against the trend — often the last bounce before a reversal — are skipped.")
trendLen = input.int(200, "Trend EMA length", minval = 20, maxval = 500, group = gL)
strongOnly = input.bool(false, "Strong breaks only", group = gL, tooltip = "Only breaks made by a strong candle (body at least one ATR) or on volume well above its recent average can lead to a signal.")

gT      = "Trade plan"
slBuf   = input.float(0.20, "Stop buffer beyond the retest (× ATR)", minval = 0.0, maxval = 2.0, step = 0.05, group = gT)
minStop = input.float(1.0, "Minimum stop (× ATR)", minval = 0.1, maxval = 3.0, step = 0.05, group = gT)
maxStop = input.float(3.0, "Maximum stop (× ATR)", minval = 0.5, maxval = 10.0, step = 0.25, group = gT, tooltip = "A retest whose stop would be wider than this is skipped.")
r1      = input.float(1.0, "TP1 (R)", minval = 0.25, maxval = 20, step = 0.25, group = gT)
r2      = input.float(2.0, "TP2 (R)", minval = 0.25, maxval = 20, step = 0.25, group = gT)
r3      = input.float(3.0, "TP3 (R)", minval = 0.25, maxval = 20, step = 0.25, group = gT)
balance = input.float(10000, "Account balance", minval = 1, step = 100, group = gT)
riskPct = input.float(1.0, "Risk per trade %", minval = 0.01, maxval = 10, step = 0.1, group = gT)
accCur  = input.string("USD", "Account currency", options = ["USD","EUR","GBP","JPY","CHF","AUD","CAD","NZD","USDT"], group = gT)
lotOver = input.float(0, "Units per lot (0 = automatic)", minval = 0, step = 1, group = gT, tooltip = "Automatic: forex 100,000 · gold 100 oz · silver 5,000 oz · futures in contracts · everything else in units.")

gV      = "Visuals"
candOn  = input.bool(true, "Glow candles", group = gV, tooltip = "Bodies drawn in four bands that brighten towards the close, in turquoise and violet. Off = your own chart candles.")
showIdle = input.bool(false, "Show unbroken levels", group = gV, tooltip = "Faint dotted lines at every level still waiting for a break. Off keeps the chart to broken levels and trades only.")
sigSize = input.string("Large", "Signal label size", options = ["Normal", "Large", "Huge"], group = gV)
tagSize = input.string("Large", "Level text size", options = ["Small", "Normal", "Large"], group = gV)
keepN   = input.int(3, "Trade plans kept on chart", minval = 1, maxval = 10, group = gV)
showHUD = input.bool(true, "Show panel", group = gV)
hudPos  = input.string("Top Right", "Panel position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left", "Middle Right"], group = gV)

sigSz = switch sigSize
    "Normal" => size.normal
    "Huge"   => size.huge
    => size.large
tagSz = switch tagSize
    "Small" => size.small
    "Large" => size.large
    => size.normal

px(float v) => str.tostring(v, format.mintick)

// ─────────────────────────────────────────────────────────────────────────
//  CORE SERIES
// ─────────────────────────────────────────────────────────────────────────
atr    = ta.atr(14)
tol    = atr * tolK
buf    = atr * brkK
volAvg = ta.sma(volume, 20)
trendE = ta.ema(close, trendLen)
hasVol = not na(volume) and volume > 0
ph     = ta.pivothigh(pivLen, pivLen)
pl     = ta.pivotlow(pivLen, pivLen)

// ─────────────────────────────────────────────────────────────────────────
//  POSITION SIZE — risk money / (stop × value of one price unit, converted
//  from the symbol's currency into the account's)
// ─────────────────────────────────────────────────────────────────────────
tkU       = str.upper(syminfo.ticker)
autoUnits = syminfo.type == "forex" ? 100000.0 : str.contains(tkU, "XAU") ? 100.0 : str.contains(tkU, "XAG") ? 5000.0 : 1.0
lotUnits  = lotOver > 0 ? lotOver : autoUnits
lotWord   = syminfo.type == "futures" ? "contracts" : lotUnits > 1 ? "lots" : "units"
fxRate    = request.currency_rate(syminfo.currency, accCur, ignore_invalid_currency = true)
fxMul     = na(fxRate) or fxRate <= 0 ? 1.0 : fxRate

lotFor(float dist) =>
    perUnit = dist * syminfo.pointvalue * fxMul
    perUnit > 0 ? balance * riskPct / 100.0 / perUnit / lotUnits : float(na)

lotTxt(float v) =>
    na(v) ? "—" : str.tostring(v, v < 10 ? "0.00" : "#,##0.0") + " " + lotWord

// ─────────────────────────────────────────────────────────────────────────
//  GLOW CANDLES — LunqFX house style, turquoise / violet
//  On a light chart the same colours are drawn one shade deeper.
// ─────────────────────────────────────────────────────────────────────────
glowBgLum   = 0.2126 * color.r(chart.bg_color) + 0.7152 * color.g(chart.bg_color) + 0.0722 * color.b(chart.bg_color)
glowIsLight = glowBgLum > 128
glowUp      = glowIsLight ? #00A99A : TURQ
glowDn      = glowIsLight ? #8A1FD1 : VIOL
glowEdge    = close >= open ? glowUp : glowDn
GLOW_NONE   = color.new(color.black, 100)
gQ = (close - open) / 4.0
plotcandle(candOn ? open : na, candOn ? math.max(open, close) : na, candOn ? math.min(open, close) : na, candOn ? close : na, "Glow mask", color = chart.bg_color, wickcolor = GLOW_NONE, bordercolor = chart.bg_color, display = display.pane)
plotcandle(candOn ? open : na, candOn ? math.max(open, open + gQ) : na, candOn ? math.min(open, open + gQ) : na, candOn ? open + gQ : na, "Glow band 1", color = color.new(glowEdge, 78), wickcolor = GLOW_NONE, bordercolor = GLOW_NONE, display = display.pane)
plotcandle(candOn ? open + gQ : na, candOn ? math.max(open + gQ, open + 2 * gQ) : na, candOn ? math.min(open + gQ, open + 2 * gQ) : na, candOn ? open + 2 * gQ : na, "Glow band 2", color = color.new(glowEdge, 58), wickcolor = GLOW_NONE, bordercolor = GLOW_NONE, display = display.pane)
plotcandle(candOn ? open + 2 * gQ : na, candOn ? math.max(open + 2 * gQ, open + 3 * gQ) : na, candOn ? math.min(open + 2 * gQ, open + 3 * gQ) : na, candOn ? open + 3 * gQ : na, "Glow band 3", color = color.new(glowEdge, 36), wickcolor = GLOW_NONE, bordercolor = GLOW_NONE, display = display.pane)
plotcandle(candOn ? open + 3 * gQ : na, candOn ? math.max(open + 3 * gQ, close) : na, candOn ? math.min(open + 3 * gQ, close) : na, candOn ? close : na, "Glow band 4", color = color.new(glowEdge, 8), wickcolor = GLOW_NONE, bordercolor = GLOW_NONE, display = display.pane)
plotcandle(candOn ? open : na, candOn ? high : na, candOn ? low : na, candOn ? close : na, "Glow wick", color = GLOW_NONE, wickcolor = color.new(glowEdge, 10), bordercolor = color.new(glowEdge, 30), display = display.pane)

// ─────────────────────────────────────────────────────────────────────────
//  LEVEL STORE
//  state  0 waiting for a break · 1 broken up, waiting for a retest from
//  above · -1 broken down, waiting for a retest from below
// ─────────────────────────────────────────────────────────────────────────
var array<float> lvPx     = array.new<float>()
var array<int>   lvTouch  = array.new<int>()
var array<int>   lvBorn   = array.new<int>()
var array<int>   lvState  = array.new<int>()
var array<int>   lvBrk    = array.new<int>()
var array<bool>  lvStrong = array.new<bool>()
var array<line>  lvLine   = array.new<line>()
var array<label> lvTag    = array.new<label>()
var array<line>  oldLines = array.new<line>()
var array<label> oldTags  = array.new<label>()

// A level that led to a trade keeps its line and name on the chart. Every
// other level — failed, expired, never broken — leaves no trace: the chart
// shows only what was traded.
dropLevel(int i, bool keep) =>
    ln = array.get(lvLine, i)
    tg = array.get(lvTag, i)
    if keep and not na(ln)
        line.set_x2(ln, bar_index)
        array.push(oldLines, ln)
        array.push(oldTags, tg)
        while array.size(oldLines) > keepN
            line.delete(array.shift(oldLines))
            label.delete(array.shift(oldTags))
    else
        line.delete(ln)
        label.delete(tg)
    array.remove(lvPx, i)
    array.remove(lvTouch, i)
    array.remove(lvBorn, i)
    array.remove(lvState, i)
    array.remove(lvBrk, i)
    array.remove(lvStrong, i)
    array.remove(lvLine, i)
    array.remove(lvTag, i)

// a new swing either joins a level already within the retest zone (one more
// touch) or becomes a level of its own
addLevel(float p, int born) =>
    joined = false
    if array.size(lvPx) > 0
        for i = 0 to array.size(lvPx) - 1
            if not joined and array.get(lvState, i) == 0 and math.abs(array.get(lvPx, i) - p) <= tol
                array.set(lvTouch, i, array.get(lvTouch, i) + 1)
                joined := true
    if not joined
        array.push(lvPx, p)
        array.push(lvTouch, 1)
        array.push(lvBorn, born)
        array.push(lvState, 0)
        array.push(lvBrk, na)
        array.push(lvStrong, false)
        array.push(lvLine, showIdle ? line.new(born, p, bar_index, p, color = color.new(MUTE, 60), width = 1, style = line.style_dotted) : line(na))
        array.push(lvTag, label(na))
        while array.size(lvPx) > maxLv
            dropLevel(0, false)

// ─────────────────────────────────────────────────────────────────────────
//  TRADE PLAN STATE
// ─────────────────────────────────────────────────────────────────────────
var int   tDir   = 0
var float tEntry = na
var float tSL    = na
var float tTP1   = na
var float tTP2   = na
var float tTP3   = na
var int   tBar   = na
var int   tStage = 0
var float tLot   = na
var string lastOut = ""

LINE_LEN = 14
var line  l0 = na
var line  l1 = na
var line  l2 = na
var line  l3 = na
var label n0 = na
var label n1 = na
var label n2 = na
var label n3 = na
var array<line>  allL = array.new<line>()
var array<label> allK = array.new<label>()
var array<label> allM = array.new<label>()

longSig  = false
shortSig = false
brkUp    = false
brkDn    = false
hitTP1   = false
hitTP2   = false
hitTP3   = false
hitSL    = false

NOBG = color.new(color.black, 100)

// a small dot on the candle where a level of the plan was reached
hitDot(float y, color c) =>
    m = label.new(bar_index, y, "", style = label.style_circle, color = c, size = size.tiny)
    array.push(allM, m)
    while array.size(allM) > keepN * 4
        label.delete(array.shift(allM))

// a level of the plan: a short line with its name in colour at the end
planLevel(float y, color c, string nm, int x2) =>
    ln = line.new(bar_index, y, x2, y, color = c, width = 2)
    lb = label.new(x2, y, nm, style = label.style_label_left, color = NOBG, textcolor = c, size = tagSz)
    [ln, lb]

moveEnd(int x) =>
    for ln in array.from(l0, l1, l2, l3)
        line.set_x2(ln, x)
    for lb in array.from(n0, n1, n2, n3)
        label.set_x(lb, x)

// ─────────────────────────────────────────────────────────────────────────
//  ENGINE — closed bars only
// ─────────────────────────────────────────────────────────────────────────
if barstate.isconfirmed
    // ── 1. the open plan, from the bar after its signal (stop first) ──
    if tDir != 0 and bar_index > tBar
        slHit = tDir == 1 ? low <= tSL : high >= tSL
        h1 = tStage < 1 and (tDir == 1 ? high >= tTP1 : low <= tTP1)
        h2 = tStage < 2 and (tDir == 1 ? high >= tTP2 : low <= tTP2)
        h3 = tDir == 1 ? high >= tTP3 : low <= tTP3
        if slHit
            hitSL := true
            hitDot(tSL, RISK)
            lastOut := "stopped"
            moveEnd(math.max(bar_index, tBar + LINE_LEN))
            tDir := 0
        else
            if h1
                tStage := 1
                hitTP1 := true
                hitDot(tTP1, GOLD)
            if h2
                tStage := 2
                hitTP2 := true
                hitDot(tTP2, GOLD)
            if h3
                hitTP3 := true
                hitDot(tTP3, GOLD)
                lastOut := "TP3 reached"
                moveEnd(math.max(bar_index, tBar + LINE_LEN))
                tDir := 0
        if tDir != 0 and not na(l0)
            moveEnd(math.max(tBar + LINE_LEN, bar_index + 3))

    // ── 2. new swings become levels (confirmed pivots — no look-ahead) ──
    if not na(ph)
        addLevel(ph, bar_index - pivLen)
    if not na(pl)
        addLevel(pl, bar_index - pivLen)

    // ── 3. walk the levels: touches, breaks, retests, expiry ──
    int   sigDir = 0
    float sigLvl = na
    if array.size(lvPx) > 0
        i = array.size(lvPx) - 1
        while i >= 0
            p  = array.get(lvPx, i)
            st = array.get(lvState, i)
            done = 0            // 1 drop quietly · 2 keep — the retest held
            if st == 0
                // a wick into the level that closes back on its side is a touch
                if (high >= p - tol and close < p and high[1] < p - tol) or (low <= p + tol and close > p and low[1] > p + tol)
                    array.set(lvTouch, i, array.get(lvTouch, i) + 1)
                upB = close > p + buf and close[1] <= p
                dnB = close < p - buf and close[1] >= p
                if upB or dnB
                    strong = math.abs(close - open) >= atr or (hasVol and volume > volAvg * 1.5)
                    array.set(lvState, i, upB ? 1 : -1)
                    array.set(lvBrk, i, bar_index)
                    array.set(lvStrong, i, strong)
                    brkUp := brkUp or upB
                    brkDn := brkDn or dnB
                    // the broken level becomes one solid line with its new name
                    line.delete(array.get(lvLine, i))
                    c = upB ? TURQ : VIOL
                    // drawn from the swing, but never more than 60 bars back
                    x0 = math.max(array.get(lvBorn, i), bar_index - 60)
                    array.set(lvLine, i, line.new(x0, p, bar_index + 3, p, color = c, width = 3))
                    array.set(lvTag, i, label.new(bar_index + 3, p, upB ? "BROKEN RESISTANCE" : "BROKEN SUPPORT", style = label.style_label_left, color = NOBG, textcolor = upB ? TURQ : #E2C2FF, size = tagSz))
                else if bar_index - array.get(lvBorn, i) > 500
                    done := 1
            else
                age    = bar_index - array.get(lvBrk, i)
                failed = st == 1 ? close < p - buf : close > p + buf
                retest = age >= 2 and (st == 1 ? low <= p + tol and close > p and close > open : high >= p - tol and close < p and close < open)
                okStr  = (not strongOnly or array.get(lvStrong, i)) and (not trendOn or (st == 1 ? close > trendE : close < trendE))
                if failed or age > winN
                    done := 1
                else if retest and sigDir == 0 and okStr
                    sigDir := st
                    sigLvl := p
                    done := 2
                else
                    line.set_x2(array.get(lvLine, i), bar_index + 3)
                    label.set_x(array.get(lvTag, i), bar_index + 3)
            if done == 0 and st == 0 and not na(array.get(lvLine, i))
                line.set_x2(array.get(lvLine, i), bar_index + 2)
            if done > 0
                dropLevel(i, done == 2)
            i -= 1

    // ── 4. the retest held: build the plan ──
    if sigDir != 0
        d = sigDir
        e = close
        sRaw = d == 1 ? math.min(low, low[1], sigLvl) - atr * slBuf : math.max(high, high[1], sigLvl) + atr * slBuf
        s = d == 1 ? math.min(sRaw, e - atr * minStop) : math.max(sRaw, e + atr * minStop)
        r = math.abs(e - s)
        if r <= atr * maxStop and tDir != d
            if tDir != 0
                lastOut := "closed on opposite signal"
                moveEnd(math.max(bar_index, tBar + LINE_LEN))
            tDir   := d
            tEntry := e
            tSL    := s
            tTP1   := e + d * r * r1
            tTP2   := e + d * r * r2
            tTP3   := e + d * r * r3
            tBar   := bar_index
            tStage := 0
            tLot   := lotFor(r)
            longSig  := d == 1
            shortSig := d == -1

            col = d == 1 ? TURQ : VIOL
            xr  = bar_index + LINE_LEN
            kS = label.new(bar_index, d == 1 ? low : high, d == 1 ? " BUY " : " SELL ", yloc = d == 1 ? yloc.belowbar : yloc.abovebar, style = d == 1 ? label.style_label_up : label.style_label_down, color = col, textcolor = d == 1 ? #03201D : color.white, size = sigSz)
            [a0, b0] = planLevel(s, RISK, "SL", xr)
            [a1, b1] = planLevel(tTP1, GOLD, "TP1", xr)
            [a2, b2] = planLevel(tTP2, color.new(GOLD, 20), "TP2", xr)
            [a3, b3] = planLevel(tTP3, color.new(GOLD, 40), "TP3", xr)
            l0 := a0
            l1 := a1
            l2 := a2
            l3 := a3
            n0 := b0
            n1 := b1
            n2 := b2
            n3 := b3
            for ln in array.from(l0, l1, l2, l3)
                array.push(allL, ln)
            for k in array.from(kS, n0, n1, n2, n3)
                array.push(allK, k)
            // every plan adds 4 lines and 5 labels — retire whole plans
            while array.size(allL) > keepN * 4
                line.delete(array.shift(allL))
            while array.size(allK) > keepN * 5
                label.delete(array.shift(allK))
            // dots older than the oldest plan still on the chart go with it
            oldest = label.get_x(array.get(allK, 0))
            while array.size(allM) > 0 and label.get_x(array.get(allM, 0)) < oldest
                label.delete(array.shift(allM))

// ─────────────────────────────────────────────────────────────────────────
//  PANEL — the retest watchlist, then the open trade
// ─────────────────────────────────────────────────────────────────────────
hudP(string s) =>
    switch s
        "Top Right"    => position.top_right
        "Top Left"     => position.top_left
        "Bottom Right" => position.bottom_right
        "Bottom Left"  => position.bottom_left
        => position.middle_right

rowL(table tb, int r, string k, string v, color kc, color vc, color bg) =>
    table.cell(tb, 0, r, "  " + k, text_color = kc, text_size = size.normal, text_halign = text.align_left, bgcolor = bg)
    table.cell(tb, 1, r, v + "  ", text_color = vc, text_size = size.normal, text_halign = text.align_right, bgcolor = bg)

var table hud = table.new(position.top_right, 2, 12)
if showHUD and barstate.islast
    table.delete(hud)
    hud := table.new(hudP(hudPos), 2, 12, bgcolor = CARD, border_color = color.new(#28323F, 0), border_width = 1)
    active = tDir != 0
    riskNow = active ? math.abs(tEntry - tSL) : na
    liveR = active and riskNow > 0 ? (close - tEntry) * tDir / riskNow : na

    // pending zones, newest first
    pend = array.new<int>()
    if array.size(lvState) > 0
        for i = array.size(lvState) - 1 to 0
            // a break past its retest window is no longer a setup, even on
            // the live bar before it is cleared at the close
            if array.get(lvState, i) != 0 and bar_index - array.get(lvBrk, i) <= winN
                array.push(pend, i)
    nPend = array.size(pend)

    heroTx = active ? (tDir == 1 ? "▲  LONG · RETEST HELD" : "▼  SHORT · RETEST HELD") : nPend > 0 ? "◆  " + str.tostring(nPend) + " BREAK" + (nPend > 1 ? "S" : "") + " · WAITING FOR RETEST" : "WATCHING LEVELS"
    heroBg = active ? (tDir == 1 ? #0A5F59 : #5B1489) : nPend > 0 ? #2A2F45 : CARD2
    table.cell(hud, 0, 0, "  BREAKOUT & RETEST  ", text_color = TURQ, text_size = size.small, text_halign = text.align_left, bgcolor = HEAD)
    table.cell(hud, 1, 0, str.tostring(riskPct, "#.##") + "% risk  ", text_color = MUTE, text_size = size.small, text_halign = text.align_right, bgcolor = HEAD)
    table.cell(hud, 0, 1, "  " + heroTx + "  ", text_color = color.white, text_size = size.large, text_halign = text.align_left, bgcolor = heroBg)
    table.merge_cells(hud, 0, 1, 1, 1)

    table.cell(hud, 0, 2, "  RETEST WATCHLIST", text_color = MUTE, text_size = size.small, text_halign = text.align_left, bgcolor = HEAD)
    table.cell(hud, 1, 2, "distance · bars left  ", text_color = MUTE, text_size = size.small, text_halign = text.align_right, bgcolor = HEAD)
    for k = 0 to 2
        r = 3 + k
        bg = k % 2 == 0 ? CARD : CARD2
        if k < nPend
            j  = array.get(pend, k)
            st = array.get(lvState, j)
            p  = array.get(lvPx, j)
            dist = atr > 0 ? math.abs(close - p) / atr : na
            left = math.max(0, winN - (bar_index - array.get(lvBrk, j)))
            c  = st == 1 ? TURQ : VIOL
            rowL(hud, r, (st == 1 ? "▲ " : "▼ ") + px(p), str.tostring(dist, "0.0") + " ATR  ·  " + str.tostring(left) + " bars", c, TXT, bg)
        else
            rowL(hud, r, "—", "", MUTE, MUTE, bg)

    table.cell(hud, 0, 6, "  TRADE", text_color = MUTE, text_size = size.small, text_halign = text.align_left, bgcolor = HEAD)
    table.cell(hud, 1, 6, "", bgcolor = HEAD)
    rowL(hud, 7, "Entry", active ? px(tEntry) : "—", MUTE, TXT, CARD)
    rowL(hud, 8, "Stop", active ? px(tSL) : "—", MUTE, RISK, CARD2)
    rowL(hud, 9, "TP1 · TP2 · TP3", active ? (tStage >= 1 ? "✓ " : "") + px(tTP1) + " · " + (tStage >= 2 ? "✓ " : "") + px(tTP2) + " · " + px(tTP3) : "—", MUTE, GOLD, CARD)
    rowL(hud, 10, "Lot", active ? lotTxt(tLot) : "—", MUTE, TURQ, CARD2)
    rowL(hud, 11, active ? "Now" : "Last plan", active and not na(liveR) ? (liveR >= 0 ? "+" : "") + str.tostring(liveR, "0.0") + " R" : lastOut == "" ? "—" : lastOut, MUTE, active and not na(liveR) ? (liveR >= 0 ? TURQ : RISK) : MUTE, CARD)

// ─────────────────────────────────────────────────────────────────────────
//  ALERTS
// ─────────────────────────────────────────────────────────────────────────
planMsg(string side) =>
    "Breakout & Retest · " + syminfo.ticker + " " + timeframe.period + " · " + side + " @ " + px(tEntry) + " · SL " + px(tSL) + " · TP1 " + px(tTP1) + " · TP2 " + px(tTP2) + " · TP3 " + px(tTP3) + " · " + lotTxt(tLot)

if longSig
    alert(planMsg("BUY"), alert.freq_once_per_bar_close)
if shortSig
    alert(planMsg("SELL"), alert.freq_once_per_bar_close)
if brkUp or brkDn
    alert("Breakout & Retest · " + syminfo.ticker + " " + timeframe.period + (brkUp ? " · level broken UP — watch for the retest" : " · level broken DOWN — watch for the retest"), alert.freq_once_per_bar_close)
if hitTP1 or hitTP2 or hitTP3
    alert("Breakout & Retest · " + syminfo.ticker + (hitTP3 ? " TP3" : hitTP2 ? " TP2" : " TP1") + " reached", alert.freq_once_per_bar_close)
if hitSL
    alert("Breakout & Retest · " + syminfo.ticker + " stop reached", alert.freq_once_per_bar_close)

alertcondition(longSig,  "Buy retest",  "Breakout & Retest: {{ticker}} {{interval}} BUY — retest held")
alertcondition(shortSig, "Sell retest", "Breakout & Retest: {{ticker}} {{interval}} SELL — retest held")
alertcondition(brkUp or brkDn, "Breakout", "Breakout & Retest: {{ticker}} {{interval}} level broken — watch for the retest")
alertcondition(hitSL, "Stop reached", "Breakout & Retest: {{ticker}} stop reached")
